package zzu.xjc.http;

import zzu.xjc.http.util.Logger;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.*;
import java.util.concurrent.TimeUnit;

public class HttpServer {
    private final int port;
    private final int workThreads;
    private boolean beforeStart = true;
    private final Acceptor acceptor = new Acceptor();
    private final Workers workers = new Workers();
    private final HashMap<HttpEndPoint, HttpService> serviceMap = new HashMap<>();
    private final HttpServiceExecutor executor = new HttpServiceExecutor();
    private final HttpFilterChain filterChain = new HttpFilterChain();
    private final BufferPool bufferPool = new DirectBufferPool();
    private final HashMap<String,HttpSession> sessionMap = new HashMap<>();
    private final FilesLRUCache filesLRUCache;

    static long CachedFileMaxSize;
    public static String ServerName = "TinyHttpServer/0.1";



    public HttpServer(int port) {
        this(port,Runtime.getRuntime().availableProcessors() * 2 );
    }
    public HttpServer(int port, int threads){
        this(port,threads,32,64 * 1024);
    }
    public HttpServer(int port,int threads,int fileCacheSize, int cachedFileMaxSize){
        this.port = (port <= 0 || port >= 65536) ? 80 : port;
        this.workThreads = (threads <= 0 || threads > 256 ) ?
                Runtime.getRuntime().availableProcessors() * 2 : threads;
        this.filesLRUCache = new FilesLRUCache(fileCacheSize,bufferPool);
        CachedFileMaxSize = cachedFileMaxSize;
    }

    public void serve(String url, Http.Method method, HttpService service){
        if (beforeStart){
            if(method.equals(Http.Method.ANY)){
                serviceMap.put(HttpEndPoint.combine(url,Http.Method.GET),service);
                serviceMap.put(HttpEndPoint.combine(url,Http.Method.PUT),service);
                return;
            }
            serviceMap.put(HttpEndPoint.combine(url,method),service);
            return;
        }
        Logger.warn("HttpServer已经开始运行了，因此无法继续配置。HttpServer is Already Running, So It Can't No Longer Be Configured.");
    }
    public void filter(String urlPattern,Http.Method method,HttpFilter filter){
        if (beforeStart){
            filterChain.addFilter(urlPattern,method,filter);
            return;
        }
        Logger.warn("HttpServer已经开始运行了，因此无法继续配置。HttpServer is Already Running, So It Can't No Longer Be Configured.");
    }
    public void staticResourceDir(String dir){
        File file = new File(dir);
        if (!file.exists()){
            Logger.warn("HttpServer staticResourceDir() Directory not exist" + file.getPath());
        }
        if (!file.isDirectory()){
            this.registerStaticResource( Http.Url.process("/"+ file.getName()) , file.getAbsolutePath());
            return;
        }
        for (File f : Objects.requireNonNull(file.listFiles())) {
            staticResource(f,"/");
        }
    }

    private void staticResource(File file,String prefix){
        if (!file.isDirectory()){
            this.registerStaticResource( Http.Url.process(prefix + file.getName()), file.getAbsolutePath());
            return;
        }
        for (File f : Objects.requireNonNull(file.listFiles())) {
            staticResource(f,prefix + file.getName() + "/");
        }
    }

    private void registerStaticResource(String url,String path){
        Logger.info( "Static Resource : " + path + " , url : " + url);
        this.serve( url , Http.Method.GET , ((req, resp) -> {
            List<ByteBuffer> list = filesLRUCache.get(url);
            if(list != null && list.size() > 0){
                resp.setBufferList(list);
                return;
            }
            try {
                FileChannel channel = new FileInputStream(path).getChannel();
                long size = channel.size();
                int bufCount = (int)Math.ceil((double) size / 1024);
                LinkedList<ByteBuffer> bufferList = bufferPool.allocate(bufCount);
                Iterator<ByteBuffer> iter = bufferList.iterator();
                do{
                    ByteBuffer buffer = iter.next();
                    int len;
                    do {
                        len = channel.read(buffer);
                    }while (len > 0);
                    if (buffer.hasRemaining() && len == 0){
                        Logger.warn("HttpServer > Static Resource > buffer.hasRemaining() && len == 0 ");
                    }
                }while (iter.hasNext());
                bufferList.forEach(ByteBuffer::flip);

                resp.setHeader(HttpResponse.Header.ContentLength, String.valueOf(size));
                String contentType = Files.probeContentType(Path.of(path));
                resp.setHeader(HttpResponse.Header.ContentType,contentType);
                ByteBuffer headerBuffer = resp.toHeaderBuffer(bufferPool);

                bufferList.addFirst(headerBuffer);
                resp.setBufferList(bufferList);
                if (size < HttpServer.CachedFileMaxSize){
                    filesLRUCache.put(url,bufferList); // Save to File LRU Cache
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }));
    }


    public void start() throws IOException {
        beforeStart = false;
        injectAll();
        initAll();
        initServer();
        acceptor.start();
        workers.start();
        new Thread(this::workRegularly,"Regular ").start();
    }
    private void injectAll(){
        acceptor.inject(workers);
        executor.inject(serviceMap,filterChain,sessionMap,bufferPool);
        workers.inject(executor,bufferPool);
    }
    private void initAll() throws IOException {
        acceptor.init(port);
        workers.init(workThreads);
    }

    private void initServer(){
        HttpEndPoint endPoint = HttpEndPoint.combine("/index.html", Http.Method.GET);
        if (serviceMap.containsKey(endPoint)){
            serviceMap.put(HttpEndPoint.combine("/", Http.Method.GET),serviceMap.get(endPoint));
        }
    }

    private void checkExpiredSession(){
        long time = System.currentTimeMillis();
        sessionMap.forEach((id, session) -> {
            if (time > session.expireTime){
                sessionMap.remove(id);
            }
        });
        Logger.info("Check All Expired Session, Consume Time : " + (System.currentTimeMillis() - time) + " ms");
    }
    void workRegularly(){
        while (true){
            try {
                TimeUnit.SECONDS.sleep(60);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            this.checkExpiredSession();
        }
    }

    private static long SessionExpireMilliSeconds = 30 * 60 * 1000;
    private static int cookieExpireSeconds = 30 * 60;
    static long SessionExpireMilliSeconds() {
        return SessionExpireMilliSeconds;
    }
    public void setSessionExpireSeconds(int sessionExpireSeconds) {
        SessionExpireMilliSeconds = sessionExpireSeconds * 1000L;
    }

    static int cookieExpireSeconds() {
        return cookieExpireSeconds;
    }
    public void setCookieExpireSeconds(int cookieExpireMilliSeconds) {
        HttpServer.cookieExpireSeconds = cookieExpireMilliSeconds;
    }
}
